-
Notifications
You must be signed in to change notification settings - Fork 5
Conversation
SW uses |
I thought I could just import the Auth module, but obviously that doesn't work. @eatyourgreens Am I right in thinking that there are no methods in OAuth currently that check sessions/refresh it if needed that don't involve an iframe (which isn't used anymore, since your latest changes to the client)? |
Nothing in my changes touched the iframe code, so it's still in use for refreshing tokens, if you allow it in your browser settings. One solution would be to mirror the call that the client makes before every request with the Auth module. apiClient.beforeEveryRequest = function() {
return oauth.checkBearerToken();
}, but you're right, the OAuth module doesn't have a |
@eatyourgreens wouldn't that solution still rely on allowing iframes in the browser? Or do you mean it as "we need to write a method similar to |
@simoneduca zooniverse/panoptes-javascript-client#76 describes authorisation in more detail. |
Atm OAuth |
Thanks, re-reading than now. |
Just realised there’s a bug in SW’s use of storage too, in that the version number is part of the storage key so that volunteers couldn’t access their stored data if the version number ever changed. Also, the old data would never be deleted. This is a completely separate issue, however. |
Cool, nice catch. How did you see that? Decoding the token? |
OAuth extends Model, which is an event emitter. There’s a little bit about it in the Readme for json-api-client too. |
The key bug? By looking at the names of the storage keys in the dev console. |
Yes. Which key? I can only see the keys |
Got it, just saw the issue. |
I was thinking of checking the token validity every time the user submits a classification. I think this would work fine for most projects, but I know some SW users can be on a single subject for more than two hours before they submit a classification. In that situation I think a warning to save their work and login before submitting should be enough. |
How about my suggestion earlier in this thread, to run the check before each request? #369 (comment) |
That's what I'd love to do and I intended my thought of checking the token before submitting classifications in that vein. But obviously I'm missing something. I don't know how to call it on every request. Do you mean to have a |
Doesn’t the code in my comment work? |
Probably, but my question is about where to put it. |
SW is your app, so that’s up to you, but it should be added wherever the API client is first configured. |
I've staged the latest changes https://preview.zooniverse.org/shakespearesworld/#!/ |
2a685ef
to
77b44b6
Compare
// @ngInject | ||
function zooAPI(zooAPIConfig) { | ||
|
||
function zooAPI($rootScope, zooAPIConfig) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a blocking comment, but what does zooAPIConfig
do? I don't see it anywhere else in this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. Seems to be doing nothing.
@@ -4,10 +4,21 @@ require('./zooapi.module.js') | |||
.factory('zooAPI', zooAPI); | |||
|
|||
var ApiClient = require('panoptes-client/lib/api-client'); | |||
|
|||
var OAuth = require('panoptes-client/lib/oauth') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Out of interest, why are instances named with capital letters here, not lowercase? That naming convention usually indicates the name of a class, not an object.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure, when I was writing them I didn't know the convention and now I'm just sticking there for consistency, which is probably terrible 🤷♂️
console.log('Failed to refresh token: ', error); | ||
alert('Your session has finished. Please save your work and login again.') | ||
$rootScope.$broadcast('auth:loginChange'); | ||
OAuth.signOut(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this call OAuth.signIn(url)
and sign them back in again with a new token?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should probably clarify here. They aren't signed out yet here, but their token is about to expire. Calling OAuth.signIn
will redirect the browser to panoptes.zooniverse.org, then back to SW with a refreshed token for another 2 hours.
@@ -66,7 +66,7 @@ function buildScript(file) { | |||
.pipe(gulpif(createSourcemap, sourcemaps.init())) | |||
.pipe(gulpif(global.isProd, streamify( | |||
uglify({ | |||
compress: { drop_console: true } | |||
compress: { } // leave console.log in prod, for testing |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change should be reverted before this is merged to master.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks okay to me, but I don't know Angular at all so did not understand the addition of $rootScope
. Happy for this to be merged if it works, once the client has been updated. The uglify config should maybe be changed back before merging this to master.
@@ -93,12 +94,7 @@ function TranscribeController($stateParams, $modal, $scope, $window, Annotations | |||
vm.subject = SubjectsFactory.current; | |||
} | |||
|
|||
function subjectLoadError(result) { | |||
if (result === 'outOfData') { | |||
$scope.$broadcast('subject:outOfData'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did you mean to delete this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did, because your pseudo-code didn't use it, so I thought deleting was implied in your comment. Was it not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That code was an example of how to catch errors in a chain of promises, that's all.
What did this line do? It looks, to me, like it notifies when the workflow is out of subjects, or something like that. Won't removing it break SW wherever it listens for this event?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. I've been looking into the same 10 lines for too long.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mind you, not adding it back in helps with stopping requests...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, you're adding it back in like this?
loadSubjects()
.catch(handleSubjectError)
.then(loadAggregations)
.catch(handleErrors)
in which case loadAggregations
will always be run, even if loadSubjects
throws an error, because you're catching errors mid-chain then recovering and continuing the chain. Read the section about Promise chains at MDN for more detail.
What you really want to be doing is interrupting the chain, then handling your errors at the end ie. use handleErrors
to handle any errors that occurred while subjects were loading, or while aggregations were loading.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I was adding it back like this:
function loadSubject() {
return SubjectsFactory.$getData($stateParams.subjectSet)
.then(subjectLoaded, subjectLoadError)
.then(loadAggregations)
.catch(handleErrors);
}
But I think it's equivalent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep, that's functionally the same as the pseudo-code I posted. A catch
block is equivalent to passing a second argument to then
. I think the MDN docs explain that too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should I just remove subjectLoadError
then (provided I find another decent place for the out-of-date
message)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I read that. That's why I said they are equivalent.
With the latest changes, if I uncomment the |
It is and I don't know how to stop it. I don't think the problem is angular-specific. |
That's what my pseudo-code example was supposed to be showing: that subjects and aggregations could be chained to the initial subject set loading, because it doesn't make sense to load subjects if subject sets aren't available. Failure anywhere would then abort the entire chain of requests. Have you talked to @shaunanoordin about how he's handling expired sessions for ASM? zooniverse/anti-slavery-manuscripts#251 is pretty much ready to merge, and it's basically this PR but for ASM. Can that code be ported across to SW, or are the two apps too different for that? |
I haven't talked to him but I had a several looks at the code and I think they are. For one thing, I can't see any other actions being chained after the promise is rejected. |
OK, alert showing only once now. One error thrown caught at the end by the handler, as expected. The only thing left to fix, I think, is the transcribe page still showing. I need to cancel the route state transition to do that. |
app/modules/auth/auth.factory.js
Outdated
alert('Your session is expired. Press OK to save your work and start a new one.') | ||
// Save any unsaved work and redirect to Panoptes for a new token. | ||
// AnnotationsFactory.updateCache(); | ||
// OAuth.signIn($location.absUrl()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is saving your work and logging back in commented out for testing?
Why do you need to cancel the transcribe page? I’m not following what the problem is there, if API errors are being handled correctly. |
Re. #369 (comment) your code doesn’t chain that function either, so you are essentially writing the same function that has already been written for ASM. |
Showing the transcribe page even in the background didn't seem very elegant. Now the error should be handled correctly. The alert shows once and on OK you're redirected, without the transcribe page being shown. |
app/modules/auth/auth.factory.js
Outdated
} | ||
// We catch any request to access the transcribe route. | ||
$transitions.onStart({ to: 'Transcribe'}, function(transition) { | ||
token = $window.sessionStorage.getItem('panoptesClientOAuth_tokenDetails'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Never, ever access the client's storage directly like this. You should always go through OAuth
in order to validate the session.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added that like because token
is still defined even when storage is cleared. Would var cachedToken = $window.sessionStorage.getItem('panoptesClientOAuth_tokenDetails');
be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely not. Don't load the client's token directly because a) you don't know what name the client code uses internally to store the token, and this may well change b) the client has a public method defined to get the token for you: checkBearerToken
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Clearing storage logs your session out, so the client logs you back in if you still have a valid Panoptes session. It doesn't simulate your session having expired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for clarify. What's the alternative then?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Set the token expires_in
to something short, or change this line in oauth.js to run the check every 30s (for example).
var TOKEN_EXPIRATION_ALLOWANCE = 119.5 * 60 * 1000;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused. If you mean "hack the local copy of the client in node_modules", I'm already doing that; if you don't mean that, I don't know what you mean, sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's exactly right. Hack the installed copy of oauth.js so that the client checks with Panoptes for a session, rather than using the stored token.
Don't forget to block third-party cookies too, since that's the bug you're addressing in this PR.
app/modules/auth/auth.factory.js
Outdated
|
||
} | ||
// We catch any request to access the transcribe route. | ||
$transitions.onStart({ to: 'Transcribe'}, function(transition) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens here if my session expires while I'm navigating to a different page, such as the home page, or if I'm already transcribing when my session expires (which is the most likely scenario)? Correct me if I'm wrong, but this block looks like it only checks if I'm still logged in when I'm about to load the transcription page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right. Removing the Transcribe
restriction will take care of navigating e.g. from /transcribe
to home /
. However, if my session expires while I'm transcribing and I click I'm done
, it'll just look like I'm still logged in and new subject will be served, which isn't right. I'll have to catch the event too.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You want to check for a valid token before every API request anyway, so just remove this check.
I think this work as expected now. I went around in circles for a while trying to checking for |
Is that the classification save failing, but other API requests still being made anyway? Each request then firing off an alert box. |
ASM have launched their fix for classification save failures today, where they open a dialog box to tell you that there was a problem, save any unfinished work to local storage then refresh to log you back in. |
@eatyourgreens would you mind having one more playing around with this please? I have looked at the AMS one more time and I'll chat with Shaun later, but I'd like to hear if you see any problems with how's the app behaving now. I could improve UI by using a modal rather than |
@simoneduca did you manage to fix the bug you noted in this comment? #369 (comment) |
No that still happens, but I wouldn't call it a bug. Poor UX rather: the classifications-options modal shows up because clicking |
Is it something that can be fixed? You said the alert was opening two or three times. |
Anyway, I can come back to this when I'm finished with the feedback PR, but that might not be this week. |
If my token expires while I'm transcribing, the alert box pops up when I try to submit my classification but I'm not logged back in after pressing OK. I'm also seeing the alert box pop up more than once. From the console errors, sign-in is trying to call this URL, which doesn't work. https://panoptes.zooniverse.org/users/sign_in/?now=1521113774236. I'm not sure why that's happening. EDIT: nevermind. localhost isn't allowed to authenticate to Panoptes production, hence the error. |
That's correct. I'm trying to solve this problem
Roger pointed out a way of solving the multiple alerts issue is to wait until all promises are resolved on submitting a classification before doing anything else. |
@rogerhutchings using
|
Not working yet, just a proof of concept.
The first I was trying to get working was calling the refresh client method after 20 seconds the project loads the home page. But if you check the logs or the network call on preview we get nothing back.